/*
 ------------------------------------------------------------------------------
 Copyright (C) 2009-2011 Eternal Games.

 This file is part of the Eternal Tech source code.

 The Eternal Tech source code is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or (at your
 option) any later version.

 The Eternal Tech source code is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 more details.

 You should have received a copy of the GNU General Public License along with
 the Eternal Tech source code; if not, write to the Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 ------------------------------------------------------------------------------
*/


//
// cl_console.cpp - ...
//


#include "cl_local.h"
#include "client.h"


#define CON_CHAR_WIDTH		5
#define CON_CHAR_HEIGHT		10

#define CON_BORDER_SIZE		1
#define CON_SPACE_SIZE		5

#define	CON_LINES			1024
#define CON_LINE_LENGTH		((SCREEN_WIDTH - CON_SPACE_SIZE * 6) / CON_CHAR_WIDTH)

#define	CON_TIMES			5

#define CON_MAX_INPUT		256
#define CON_MAX_HISTORY		32
#define CON_MAX_CMDS		8192

struct console_t {
	bool			initialized;

	float			opacity;

	int				height;

	int				currentLine;		// Line where next message will be printed
	int				displayLine;		// Bottom of console displays this line

	bool			carriageReturn;

	int				colorIndex;			// Current color index

	// Text buffer
	short			text[CON_LINES][CON_LINE_LENGTH];
	int				colPos;

	// Transparent notify lines
	int				notifyTimes[CON_TIMES];

	// Input buffer
	char			inputText[CON_MAX_INPUT];
	int				inputLength;
	int				inputColPos;

	// History buffer
	char			historyText[CON_MAX_HISTORY][CON_MAX_INPUT];
	int				historyCount;
	int				historyLine;

	// Command auto-completion
	char			cmdPartial[CON_MAX_INPUT];
	int				cmdPartialLen;
	int				cmdPartialArg;

	char			cmdName[CON_MAX_INPUT];
	char			cmdArgs[CON_MAX_INPUT];
	const char		*cmdList[CON_MAX_CMDS];
	int				cmdCount;
	int				cmdIndex;
	int				cmdCursor;
};

static console_t	con;

static cvar_t		*con_noNotify;
static cvar_t		*con_notifyTime;
static cvar_t		*con_speed;


/*
 ==============================================================================

    RESET FUNCTIONS

 ==============================================================================
*/


/*
 ==================
 Con_ClearNotify
 ==================
*/
static void Con_ClearNotify ()
{
	int		i;

	for (i = 0; i < CON_TIMES; i++)
		con.notifyTimes[i] = 0;
}

/*
 ==================
 Con_ClearInput
 ==================
*/
static void Con_ClearInput ()
{
	con.inputText[0] = 0;
	con.inputLength = 0;
	con.inputColPos = 0;
}

/*
 ==================
 Con_ClearCommands
 ==================
*/
static void Con_ClearCommands ()
{
	int		i;

	con.cmdName[0] = 0;
	con.cmdArgs[0] = 0;

	for (i = 0; i < con.cmdCount; i++)
		Mem_Free(con.cmdList[i]);

	con.cmdCount = 0;
	con.cmdIndex = -1;
	con.cmdCursor = -1;
}


/*
 ==============================================================================

    TEXT BUFFER

 ==============================================================================
*/


/*
 ==================
 Con_CarriageReturn
 ==================
*/
static void Con_CarriageReturn ()
{
	int		i;

	// Clear the line
	for (i = 0; i < CON_LINE_LENGTH; i++)
		con.text[con.currentLine % CON_LINES][i] = 0;

	con.colPos = 0;

	// Mark time for transparent overlay
	con.notifyTimes[con.currentLine % CON_TIMES] = cls2.realtime;
}

/*
 ==================
 Con_LineFeed
 ==================
*/
static void Con_LineFeed ()
{
	int		i;

	// Scroll down and advance
	if (con.displayLine == con.currentLine)
		con.displayLine++;

	con.currentLine++;

	if (con.displayLine < con.currentLine - CON_LINES + 1)
		con.displayLine = con.currentLine - CON_LINES + 1;
	if (con.displayLine < CON_LINES)
		con.displayLine = CON_LINES;

	// Clear the line
	for (i = 0; i < CON_LINE_LENGTH; i++)
		con.text[con.currentLine % CON_LINES][i] = 0;

	con.colPos = 0;

	// Mark time for transparent overlay
	con.notifyTimes[con.currentLine % CON_TIMES] = cls2.realtime;
}

/*
 ==================
 Con_Print
 ==================
*/
void Con_Print (const char *text)
{
	int		ch, length;
	int		i;

	if (!con.initialized)
		return;

	while (*text){
		ch = *(const byte *)text;

		// Set the color index if needed
		if (Str_IsColor(text))
		{
			con.colorIndex = Str_ColorIndexForChar(text[1]);
			text += 2;
			continue;
		}

		// Count word length
		for (length = 0; length < CON_LINE_LENGTH; length++)
		{
			if (Str_CharIsWhiteSpace(text[length]))
				break;
		}

		// Word wrap
		if (length != CON_LINE_LENGTH && con.colPos + length > CON_LINE_LENGTH)
			con.colPos = 0;

		// Handle carriage return and line feed
		if (con.carriageReturn)
		{
			con.carriageReturn = false;

			if (ch != '\n')
				Con_CarriageReturn();
		}
		else
		{
			if (!con.colPos)
				Con_LineFeed();
		}

		// Store character
		switch (ch){
		case '\r':
			con.carriageReturn = true;

			con.colorIndex = COLOR_DEFAULT;
			con.colPos = 0;

			break;
		case '\n':
			con.colorIndex = COLOR_DEFAULT;
			con.colPos = 0;

			break;
		case '\t':
			for (i = 0; i < 4; i++)
			{
				if (con.colPos == CON_LINE_LENGTH)
					break;

				con.text[con.currentLine % CON_LINES][con.colPos++] = (con.colorIndex << 8) | ' ';
			}

			if (con.colPos == CON_LINE_LENGTH)
				con.colPos = 0;

			break;
		default:
			con.text[con.currentLine % CON_LINES][con.colPos++] = (con.colorIndex << 8) | ch;

			if (con.colPos == CON_LINE_LENGTH)
				con.colPos = 0;

			break;
		}

		// Advance to the next character
		text++;
	}

	// Mark time for transparent overlay
	con.notifyTimes[con.currentLine % CON_TIMES] = cls2.realtime;
}


/*
 ==============================================================================

    COMMAND AUTO-COMPLETION

 ==============================================================================
*/


/*
 ==================
 Con_SortCommandList
 ==================
*/
static int Con_SortCommandList (const void *elem1, const void *elem2)
{
	const char	*cmd1 = *(const char **)elem1;
	const char	*cmd2 = *(const char **)elem2;

	return Str_Compare(cmd1, cmd2);
}

/*
 ==================
 Con_CompleteCommandCallback
 ==================
*/
static void Con_CompleteCommandCallback (const char *string)
{
	int		i;

	if (con.cmdCount == CON_MAX_CMDS)
		return;   // Too many commands

	if (Str_ICompareChars(string, con.cmdPartial, con.cmdPartialLen))
		return;   // No match

	// Ignore duplicates
	for (i = 0; i < con.cmdCount; i++)
	{
		if (!Str_ICompare(con.cmdList[i], string))
			return;   // Already in list
	}

	// Add it
	con.cmdList[con.cmdCount++] = Mem_DupString(string, cls.consoleMemoryPool);
}

/*
 ==================
 Con_CompleteCommand
 ==================
*/
static void Con_CompleteCommand ()
{
	cvar_t	*cvar;
	char	partial[CON_MAX_INPUT];
	int		length;
	int		i;

	// Clear auto-complete list
	Con_ClearCommands();

	// Tokenize the string
	Cmd_TokenizeString(con.inputText, false);

	if (!Cmd_Argc())
		return;   // Nothing to search for

	// Find matching arguments if possible
	Str_Copy(con.cmdPartial, Cmd_Argv(1), sizeof(con.cmdPartial));
	con.cmdPartialLen = Str_Length(con.cmdPartial);
	con.cmdPartialArg = 1;

	//Cmd_ArgumentCompletion(Con_CompleteCommandCallback);
	CVar_ArgumentCompletion(Con_CompleteCommandCallback);

	// Otherwise find matching commands and variables
	if (!con.cmdCount)
	{
		Str_Copy(con.cmdPartial, Cmd_Argv(0), sizeof(con.cmdPartial));
		con.cmdPartialLen = Str_Length(con.cmdPartial);
		con.cmdPartialArg = 0;

		//Cmd_CommandCompletion(Con_CompleteCommandCallback);
		CVar_CommandCompletion(Con_CompleteCommandCallback);
	}

	if (!con.cmdCount)
		return;   // Nothing was found

	// Copy the name and arguments if available
	if (con.cmdPartialArg)
		Str_Copy(con.cmdName, Cmd_Argv(0), sizeof(con.cmdName));

	if (con.cmdPartialArg + 1 != Cmd_Argc())
		Str_Copy(con.cmdArgs, Cmd_Args(), sizeof(con.cmdArgs));   // FIXME: Cmd_Args(con.cmdPartialArg + 1, -1, false)

	if (con.cmdCount == 1)
	{
		// Only one was found, so copy it to the input line
		if (con.cmdName[0])
		{
			if (con.cmdArgs[0])
				Str_SPrintf(con.inputText, sizeof(con.inputText), "%s %s %s", con.cmdName, con.cmdList[0], con.cmdArgs);
			else
				Str_SPrintf(con.inputText, sizeof(con.inputText), "%s %s", con.cmdName, con.cmdList[0]);
		}
		else
		{
			if (con.cmdArgs[0])
				Str_SPrintf(con.inputText, sizeof(con.inputText), "%s %s", con.cmdList[0], con.cmdArgs);
			else
				Str_SPrintf(con.inputText, sizeof(con.inputText), "%s ", con.cmdList[0]);
		}

		con.inputLength = Str_Length(con.inputText);
		con.inputColPos = con.inputLength;
	}
	else
	{
		// Sort the commands
		qsort(con.cmdList, con.cmdCount, sizeof(char *), Con_SortCommandList);

		// Print the commands
		if (con.cmdName[0])
			Com_Printf("]%s %s\n", Cmd_Argv(0), Cmd_Argv(1));
		else
			Com_Printf("]%s\n", Cmd_Argv(0));

		for (i = 0; i < con.cmdCount; i++)
		{
			if (con.cmdName[0])
				Com_Printf("    %s %s\n", con.cmdName, con.cmdList[i]);
			else
			{
				cvar = CVar_FindVariable(con.cmdList[i]);
				if (cvar)
					Com_Printf("    %s = \"%s" S_COLOR_DEFAULT "\"\n", con.cmdList[i], cvar->value);
				else
					Com_Printf("    %s\n", con.cmdList[i]);
			}
		}

		if (con.cmdCount == CON_MAX_CMDS)
			Com_Printf("    ...\n");

		// Find the number of matching characters between the first and last
		// commands in the list and copy them
		length = 0;

		while (1){
			if (!con.cmdList[0][length] || !con.cmdList[con.cmdCount-1][length])
				break;

			if (con.cmdList[0][length] != con.cmdList[con.cmdCount-1][length])
				break;

			partial[length] = con.cmdList[0][length];
			length++;
		}

		partial[length] = 0;

		if (con.cmdName[0])
			length += Str_Length(con.cmdName) + 1;

		// Save the cursor position
		con.cmdCursor = length;

		// Copy the match to the input line
		if (con.cmdName[0])
		{
			if (con.cmdArgs[0])
				Str_SPrintf(con.inputText, sizeof(con.inputText), "%s %s %s", con.cmdName, partial, con.cmdArgs);
			else
				Str_SPrintf(con.inputText, sizeof(con.inputText), "%s %s", con.cmdName, partial);
		}
		else
		{
			if (con.cmdArgs[0])
				Str_SPrintf(con.inputText, sizeof(con.inputText), "%s %s", partial, con.cmdArgs);
			else
				Str_SPrintf(con.inputText, sizeof(con.inputText), "%s", partial);
		}

		con.inputLength = Str_Length(con.inputText);
		con.inputColPos = length;
	}
}


/*
 ==============================================================================

    KEY EVENTS

 ==============================================================================
*/


/*
 ==================
 Con_CharEvent

 TODO: Move clipboard paste into Con_KeyEvent?
 ==================
*/
void Con_CharEvent (int ch)
{
	const char   *text;
	int			 i, j;

	if (!con.initialized)
		return;

	if (!con.opacity)
		return;

	// Clipboard paste
	if (ch == 'v' - 'a' + 1)
	{
		text = Win_GetClipboardText();
		if (!text)
			return;

		for (j = 0; text[j]; j++)
		{
			if (!Str_CharIsPrintable(text[j]))
				continue;

			if (con.inputLength == CON_MAX_INPUT - 1)
				break;

			for (i = con.inputLength; i > con.inputColPos; i--)
				con.inputText[i] = con.inputText[i-1];

			con.inputLength++;

			con.inputText[con.inputColPos++] = text[j];
			con.inputText[con.inputLength] = 0;
		}

		Mem_Free(text);

		// Clear auto-complete list
		Con_ClearCommands();

		return;
	}

	// Ignore none printble characters
	if (!Str_CharIsPrintable(ch))
		return;

	// Add a new character to the buffer
	if (con.inputLength == CON_MAX_INPUT - 1)
		return;

	for (i = con.inputLength; i > con.inputColPos; i--)
		con.inputText[i] = con.inputText[i-1];

	con.inputLength++;

	con.inputText[con.inputColPos++] = ch;
	con.inputText[con.inputLength] = 0;

	// Clear auto-complete list
	Con_ClearCommands();
}

/*
 ==================
 Con_KeyEvent
 ==================
*/
void Con_KeyEvent (int key, bool down)
{
	int	    i;

	if (!con.initialized)
		return;

	if (!down)
		return;   // Ignore key up events

	// Auto-complete command
	if (key == K_TAB)
	{
		if (con.cmdCount <= 1)
			Con_CompleteCommand();
		else
		{
			// Cycle through the commands
			if (Key_IsDown(K_LSHIFT) || Key_IsDown(K_RSHIFT))
			{
				if (--con.cmdIndex < 0)
					con.cmdIndex = con.cmdCount - 1;
			}
			else
			{
				if (++con.cmdIndex == con.cmdCount)
					con.cmdIndex = 0;
			}

			if (con.cmdName[0])
			{
				if (con.cmdArgs[0])
					Str_SPrintf(con.inputText, sizeof(con.inputText), "%s %s %s", con.cmdName, con.cmdList[con.cmdIndex], con.cmdArgs);
				else
					Str_SPrintf(con.inputText, sizeof(con.inputText), "%s %s", con.cmdName, con.cmdList[con.cmdIndex]);
			}
			else
			{
				if (con.cmdArgs[0])
					Str_SPrintf(con.inputText, sizeof(con.inputText), "%s %s", con.cmdList[con.cmdIndex], con.cmdArgs);
				else
					Str_SPrintf(con.inputText, sizeof(con.inputText), "%s", con.cmdList[con.cmdIndex]);
			}

			con.inputLength = Str_Length(con.inputText);

			if (con.cmdName[0])
				con.inputColPos = Str_Length(con.cmdName) + 1 + Str_Length(con.cmdList[con.cmdIndex]);
			else
				con.inputColPos = Str_Length(con.cmdList[con.cmdIndex]);
		}

		return;
	}

	// Execute a command
	if (key == K_ENTER || key == K_KP_ENTER)
	{
		if (!con.inputText[0])
			return;

		Com_Printf("]%s\n", con.inputText);

		// Add the command text
		Cbuf_AddText(con.inputText);
		Cbuf_AddText("\n");

		// Add to history
		Str_Copy(con.historyText[con.historyCount % CON_MAX_HISTORY], con.inputText, sizeof(con.historyText[con.historyCount % CON_MAX_HISTORY]));
		con.historyCount++;
		con.historyLine = con.historyCount;

		// Clear input
		Con_ClearInput();

		// Clear auto-complete list
		Con_ClearCommands();

		return;
	}

	// Select previous history line
	if (key == K_UPARROW || key == K_KP_UPARROW)
	{
		if (con.historyLine > 0 && con.historyCount - con.historyLine < CON_MAX_HISTORY)
			con.historyLine--;

		Str_Copy(con.inputText, con.historyText[con.historyLine % CON_MAX_HISTORY], sizeof(con.inputText));
		con.inputLength = Str_Length(con.inputText);
		con.inputColPos = con.inputLength;

		// Clear auto-complete list
		Con_ClearCommands();

		return;
	}

	// Select next history line
	if (key == K_DOWNARROW || key == K_KP_DOWNARROW)
	{
		if (con.historyLine == con.historyCount)
			return;

		con.historyLine++;

		if (con.historyLine == con.historyCount)
			Con_ClearInput();
		else
		{
			Str_Copy(con.inputText, con.historyText[con.historyLine % CON_MAX_HISTORY], sizeof(con.inputText));
			con.inputLength = Str_Length(con.inputText);
			con.inputColPos = con.inputLength;
		}

		// Clear auto-complete list
		Con_ClearCommands();

		return;
	}

	// Scroll buffer up
	if (key == K_PAGEUP || key == K_KP_PAGEUP || key == K_MWHEELUP)
	{
		if (Key_IsDown(K_LCTRL) || Key_IsDown(K_RCTRL))
			con.displayLine -= 6;
		else
			con.displayLine -= 2;

		if (con.displayLine < con.currentLine - CON_LINES + 1)
			con.displayLine = con.currentLine - CON_LINES + 1;
		if (con.displayLine < CON_LINES)
			con.displayLine = CON_LINES;

		return;
	}

	// Scroll buffer down
	if (key == K_PAGEDOWN || key == K_KP_PAGEDOWN || key == K_MWHEELDOWN)
	{
		if (Key_IsDown(K_LCTRL) || Key_IsDown(K_RCTRL))
			con.displayLine += 6;
		else
			con.displayLine += 2;

		if (con.displayLine > con.currentLine)
			con.displayLine = con.currentLine;

		return;
	}

	// Previous character
	if (key == K_LEFTARROW || key == K_KP_LEFTARROW)
	{
		if (con.inputColPos)
			con.inputColPos--;

		// Clear auto-complete list
		Con_ClearCommands();

		return;
	}

	// Next character
	if (key == K_RIGHTARROW || key == K_KP_RIGHTARROW)
	{
		if (con.inputColPos < con.inputLength)
			con.inputColPos++;

		// Clear auto-complete list
		Con_ClearCommands();

		return;
	}

	// Delete previous character
	if (key == K_BACKSPACE)
	{
		if (!con.inputColPos)
			return;

		con.inputColPos--;

		for (i = con.inputColPos; i < con.inputLength; i++)
			con.inputText[i] = con.inputText[i+1];

		con.inputLength--;

		// Clear auto-complete list
		Con_ClearCommands();

		return;
	}

	// Delete next character
	if (key == K_DELETE || key == K_KP_DELETE)
	{
		if (con.inputColPos == con.inputLength)
			return;

		for (i = con.inputColPos; i < con.inputLength; i++)
			con.inputText[i] = con.inputText[i+1];

		con.inputLength--;

		// Clear auto-complete list
		Con_ClearCommands();

		return;
	}
}


/*
 ==============================================================================

    DRAWING

 ==============================================================================
*/


/*
 ==================
 Con_Close
 ==================
*/
void Con_Close ()
{
	if (!con.initialized)
		return;

	con.opacity = 0.0f;

	Con_ClearNotify();
	Con_ClearInput();
	Con_ClearCommands();

	Key_RemoveCatchers(KEYCATCH_CONSOLE);
}

/*
 ==================
 Con_Toggle
 ==================
*/
void Con_Toggle ()
{
	if (!con.initialized)
		return;

	Con_ClearNotify();
	Con_ClearInput();
	Con_ClearCommands();

	if (Key_GetCatchers() & KEYCATCH_CONSOLE)
		Key_RemoveCatchers(KEYCATCH_CONSOLE);
	else
		Key_AddCatchers(KEYCATCH_CONSOLE);
}

/*
 ==================
 Con_Run
 ==================
*/
void Con_Run ()
{
	float   opacity;

	if (!con.initialized)
		return;

	// Decide on the opacity of the console
	if (Key_GetCatchers() & KEYCATCH_CONSOLE)
		opacity = 1.0f;
	else
		opacity = 0.0f;

	if (opacity < con.opacity)
	{
		con.opacity -= con_speed->floatValue * cls2.frametime;
		if (con.opacity < opacity)
			con.opacity = opacity;
	}
	else if (opacity > con.opacity)
	{
		con.opacity += con_speed->floatValue * cls2.frametime;
		if (con.opacity > opacity)
			con.opacity = opacity;
	}
}

static void Con_DrawNotify ()
{
}

/*
 ==================
 Con_DrawInputField
 ==================
*/
static void Con_DrawInputField ()
{
	color_t		 textColor;
	const char   *text = con.inputText;
	int			 colorIndex, oldColorIndex;
	int			 cursor;
	int			 width, scroll;
	int			 x, y;
	int			 i;

	width = ((SCREEN_WIDTH - (CON_SPACE_SIZE << 2)) / CON_CHAR_WIDTH) - 2;

	y = (SCREEN_HEIGHT >> 1) - (CON_CHAR_HEIGHT + (CON_SPACE_SIZE << 1));

	// Find the cursor position
	cursor = con.inputColPos;

	for (i = 0; i < con.inputColPos; i++)
	{
		if (!Str_IsColor(&text[i]))
			continue;

		if (i == con.inputColPos - 1)
			cursor -= 1;
		else
			cursor -= 2;
	}

	// Prestep if horizontally scrolling
	colorIndex = COLOR_WHITE;

	if (cursor > width)
	{
		scroll = cursor - width;

		cursor -= scroll;

		while (*text && scroll){
			if (Str_IsColor(text))
			{
				colorIndex = Str_ColorIndexForChar(text[1]);

				text += 2;
				continue;
			}

			text++;
			scroll--;
		}
	}

	if (con.inputColPos != con.inputLength)
		width++;

	// Draw the input prompt character
	MakeRGBA(textColor, 255, 255, 255, 128 * con.opacity);

	CL_DrawChar(CON_CHAR_WIDTH, y, 0.18f, ']', textColor, cls.media.genericFont, 0);

	// Draw the input text
	x = 0;

	oldColorIndex = colorIndex;
	MakeRGBA(textColor, colorTable[colorIndex][0], colorTable[colorIndex][1], colorTable[colorIndex][2], colorTable[colorIndex][3] * con.opacity);

	while (*text && x < width){
		if (Str_IsColor(text))
		{
			colorIndex = Str_ColorIndexForChar(text[1]);

			if (colorIndex != oldColorIndex)
			{
				oldColorIndex = colorIndex;
				MakeRGBA(textColor, colorTable[colorIndex][0], colorTable[colorIndex][1], colorTable[colorIndex][2], colorTable[colorIndex][3] * con.opacity);
			}

			text += 2;
			continue;
		}

		CL_DrawChar((x + 2) * CON_CHAR_WIDTH, y, 0.18f, *text, textColor, cls.media.genericFont, 0);

		text++;
		x++;
	}

	// Draw the cursor position
	if ((cls2.realtime >> 9) & 1)
	{
		MakeRGBA(textColor, 255, 255, 255, 128 * con.opacity);

		CL_DrawChar((cursor + 2) * CON_CHAR_WIDTH, y, 0.18f, '_', textColor, cls.media.genericFont, 0);
	}
}

/*
 ==================
 Con_Draw

 TODO: version string and text adjustment
 ==================
*/
void Con_Draw ()
{
	color_t		  consoleColor = {64, 64, 64, 128 * con.opacity};
	color_t		  textColor;
	const short   *text;
	int			  colorIndex, oldColorIndex;
	int			  lines, line;
	int			  ch, x, y;
	int			  i;

	if (!con.initialized)
		return;

	// If not visible, draw notify lines
	if (!con.opacity)
	{
		Con_DrawNotify();
		return;
	}

	// Set console height
	con.height = SCREEN_HEIGHT * 0.5f;

	// Draw the background
	CL_DrawPic(0.0f, 0.0f, SCREEN_WIDTH, con.height, consoleColor, cls.media.whiteMaterial);

	// Draw the version string

	// Draw the text from the top down
	y = CON_SPACE_SIZE << 1;

	y += CON_SPACE_SIZE;
	con.height -= CON_SPACE_SIZE;

	lines = con.height / CON_CHAR_HEIGHT - 3;

	oldColorIndex = COLOR_DEFAULT;
	MakeRGBA(textColor, colorTable[COLOR_DEFAULT][0], colorTable[COLOR_DEFAULT][1], colorTable[COLOR_DEFAULT][2], colorTable[COLOR_DEFAULT][3] * con.opacity);

	for (line = con.displayLine - lines + 1; line <= con.displayLine; line++, y += CON_CHAR_HEIGHT)
	{
		if (line < 0 || con.currentLine - line >= CON_LINES)
			continue;

		text = con.text[line % CON_LINES];

		for (i = 0, x = CON_CHAR_WIDTH; i < CON_LINE_LENGTH; i++, x += CON_CHAR_WIDTH)
		{
			ch = text[i] & 0xFF;
			if (ch == 0)
				break;

			if (ch == ' ')
				continue;

			colorIndex = (text[i] >> 8) & COLOR_MASK;

			if (colorIndex != oldColorIndex)
			{
				oldColorIndex = colorIndex;
				MakeRGBA(textColor, colorTable[colorIndex][0], colorTable[colorIndex][1], colorTable[colorIndex][2], colorTable[colorIndex][3] * con.opacity);
			}

			CL_DrawChar(x, y, 0.18f, ch, textColor, cls.media.genericFont, 0);
		}
	}

	// Draw the input prompt, text, and cursor
	Con_DrawInputField();
}


/*
 ==============================================================================

    CONSOLE COMMANDS

 ==============================================================================
*/


/*
 ==================
 Con_Clear_f
 ==================
*/
static void Con_Clear_f ()
{
	int		i, j;

	con.currentLine = CON_LINES - 1;
	con.displayLine = CON_LINES - 1;

	for (i = 0; i < CON_LINES; i++)
	{
		for (j = 0; j < CON_LINE_LENGTH; j++)
			con.text[i][j] = 0;
	}
}

/*
 ==================
 Con_Dump_f
 ==================
*/
static void Con_Dump_f ()
{
	fileHandle_t   f;
	char		   name[MAX_PATH_LENGTH];
	char		   line[CON_LINE_LENGTH + 1];
	short		   *text;
	int			   i, length;

	if (Cmd_Argc() != 2)
	{
		Com_Printf("Usage: conDump <fileName>\n");
		return;
	}

	Str_Copy(name, Cmd_Argv(1), sizeof(name));
	Str_DefaultFileExtension(name, sizeof(name), ".txt");

	FS_OpenFile(name, FS_WRITE, &f);
	if (!f)
	{
		Com_Printf("Couldn't write %s\n", name);
		return;
	}

	// Skip empty lines
	for (i = con.currentLine - CON_LINES + 1; i <= con.currentLine; i++)
	{
		if (con.text[i % CON_LINES][0] & 0xFF)
			break;
	}

	// Write the remaining lines
	for ( ; i <= con.currentLine; i++)
	{
		text = con.text[i % CON_LINES];

		for (length = 0; length < CON_LINE_LENGTH; length++)
		{
			line[length] = text[length] & 0xFF;

			if (!line[length])
				break;
		}

		line[length] = 0;

		FS_Printf(f, "%s" NEWLINE, line);
	}

	FS_CloseFile(f);

	Com_Printf("Dumped console text to %s\n", name);
}


/*
 ==============================================================================

    INITIALIZATION AND SHUTDOWN

 ==============================================================================
*/


/*
 ==================
 Con_Init
 ==================
*/
void Con_Init ()
{
	// Register variables
	con_noNotify = CVar_Register("con_noNotify", "1", CVAR_BOOL, 0, "Don't print notify messages", 0, 0);
	con_notifyTime = CVar_Register("con_notifyTime", "3.0", CVAR_FLOAT, 0, "Time in seconds to display notify messages on-screen", 1.0f, 60.0f);
	con_speed = CVar_Register("con_speed", "3.0", CVAR_FLOAT, 0, "Speed at which the console is faded in/out", 0.1f, 10.0f);

	// Add commands
	Cmd_AddCommand("clear", Con_Clear_f);
	Cmd_AddCommand("conDump", Con_Dump_f);

	// Set up the text buffer
	con.currentLine = CON_LINES - 1;
	con.displayLine = CON_LINES - 1;

	con.colorIndex = COLOR_DEFAULT;

	// Clear notify lines, input, and auto-complete list
	Con_ClearNotify();
	Con_ClearInput();
	Con_ClearCommands();

	con.initialized = true;

	Com_Printf("Console Initialized\n");
}

/*
 ==================
 Con_Shutdown
 ==================
*/
void Con_Shutdown ()
{
	int		i;

	if (!con.initialized)
		return;

	// Remove commands
	Cmd_RemoveCommand("clear");
	Cmd_RemoveCommand("conDump");

	// Free the auto-complete list
	for (i = 0; i < con.cmdCount; i++)
		Mem_Free(con.cmdList[i]);

	memset(&con, 0, sizeof(console_t));
}